/*
 * Copyright 2002-2005 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.hasor.dataql.sqlproc.dynamic.rule;
import net.hasor.dataql.sqlproc.dialect.BoundSqlBuilder;
import net.hasor.dataql.sqlproc.dynamic.DynamicContext;
import net.hasor.dataql.sqlproc.dynamic.SqlArg;
import net.hasor.dataql.sqlproc.dynamic.SqlMode;
import net.hasor.dataql.sqlproc.types.TypeHandler;
import net.hasor.dataql.sqlproc.types.TypeHandlerRegistry;

import java.sql.SQLException;
import java.sql.Types;
import java.util.*;

/**
 * 如果参数不为空，则生成 'and column = ?' 或者 'column = ?' 。
 * @version : 2021-06-05
 * @author 赵永春 (zyc@hasor.net)
 */
public abstract class ConditionRule implements SqlBuildRule {
    private static final TypeHandler<?> stringTypeHandler = TypeHandlerRegistry.DEFAULT.getTypeHandler(String.class);
    private final        String         prefix;

    protected ConditionRule(String prefix) {
        this.prefix = prefix;
    }

    @Override
    public boolean test(Map<String, Object> data, DynamicContext context, String activeExpr) {
        return true;
    }

    @Override
    public void executeRule(Map<String, Object> data, DynamicContext context, BoundSqlBuilder sqlBuilder, String activeExpr, String ruleValue) throws SQLException {
        ParsedSql parsedSql = ParsedSql.getParsedSql(activeExpr);
        String buildSql = parsedSql.buildSql(data);
        Object[] objects = parsedSql.buildValues(data);

        List<SqlArg> argList = new ArrayList<>(objects.length);
        boolean needExit = true;
        for (Object argData : objects) {
            SqlArg sqlArg = null;
            if (argData == null) {
                sqlArg = new SqlArg(ruleValue, null, SqlMode.In, Types.NULL, String.class, stringTypeHandler);
            } else {
                Class<?> argType = argData.getClass();
                int sqlType = TypeHandlerRegistry.toSqlType(argType);
                TypeHandler<?> typeHandler = context.findTypeHandler(argType);
                sqlArg = new SqlArg("", argData, SqlMode.In, sqlType, argType, typeHandler);
                needExit = false;
            }
            argList.add(sqlArg);
        }

        if (needExit) {
            return;
        }

        String sql = sqlBuilder.getSqlString().toLowerCase();
        if (sql.contains("where")) {
            if (sql.trim().endsWith("where") || sql.trim().endsWith("and") || sql.trim().endsWith("or")) {
                sqlBuilder.appendSql(buildSql, argList.toArray(new SqlArg[0]));
            } else {
                sqlBuilder.appendSql(this.prefix + " " + buildSql, argList.toArray(new SqlArg[0]));
            }
        } else {
            sqlBuilder.appendSql("where " + buildSql, argList.toArray(new SqlArg[0]));
        }
    }

    /** Holds information about a parsed SQL statement. */
    private static class ParsedSql {
        private final String       originalSql;
        private       int          namedParameterCount;
        private       int          unnamedParameterCount;
        private       int          totalParameterCount;
        private       List<String> parameterNames;
        private       List<int[]>  parameterIndexes;

        private ParsedSql(String originalSql) {
            this.originalSql = originalSql;
        }

        public String getOriginalSql() {
            return this.originalSql;
        }

        public int getNamedParameterCount() {
            return this.namedParameterCount;
        }

        public int getUnnamedParameterCount() {
            return this.unnamedParameterCount;
        }

        public int getTotalParameterCount() {
            return this.totalParameterCount;
        }

        public List<String> getParameterNames() {
            return this.parameterNames;
        }

        public List<int[]> getParameterIndexes() {
            return this.parameterIndexes;
        }

        /** 生成SQL */
        public String buildSql() {
            return this.buildSql(null);
        }

        /** 生成SQL */
        public String buildSql(final Map<String, ?> paramSource) {
            String originalSql = this.getOriginalSql();
            List<String> parameterNames = this.getParameterNames();
            List<int[]> parameterIndexes = this.getParameterIndexes();
            //
            StringBuilder sqlToUse = new StringBuilder();
            int lastIndex = 0;
            for (int i = 0; i < parameterNames.size(); i++) {
                int[] indexes = parameterIndexes.get(i);
                int startIndex = indexes[0];
                int endIndex = indexes[1];
                sqlToUse.append(originalSql, lastIndex, startIndex);

                if (paramSource != null) {
                    Object value = paramSource.get(parameterNames.get(i));
                    if (this.namedParameterCount > 0) {
                        if (value instanceof Iterable) {
                            Iterator<?> entryIter = ((Iterable<?>) value).iterator();
                            int k = 0;
                            while (entryIter.hasNext()) {
                                if (k > 0) {
                                    sqlToUse.append(", ");
                                }
                                k++;
                                sqlToUse.append("?");
                                entryIter.next();
                            }
                        } else {
                            sqlToUse.append("?");
                        }
                    } else {
                        sqlToUse.append("?");
                    }
                } else {
                    sqlToUse.append("?");
                }

                lastIndex = endIndex;
            }
            sqlToUse.append(originalSql.substring(lastIndex));
            return sqlToUse.toString();
        }

        /** 生成Values */
        public Object[] buildValues(final Map<String, ?> paramSource) throws SQLException {
            String originalSql = this.getOriginalSql();
            List<String> parameterNames = this.getParameterNames();
            int namedParameterCount = this.getNamedParameterCount();//带有名字参数的总数
            int unnamedParameterCount = this.getUnnamedParameterCount();//无名字参数总数
            int totalParameterCount = this.getTotalParameterCount();//参数总数
            //
            List<Object> paramArray = new ArrayList<>(totalParameterCount);
            if (namedParameterCount > 0 && unnamedParameterCount > 0) {
                throw new SQLException("You can't mix named and traditional ? placeholders. You have " + namedParameterCount + " named parameter(s) and " + unnamedParameterCount + " traditonal placeholder(s) in [" + originalSql + "]");
            }
            for (String paramName : parameterNames) {
                Object value = paramSource.get(paramName);
                if (value instanceof Iterable) {
                    for (Object o : (Iterable<?>) value) {
                        paramArray.add(o);
                    }
                } else {
                    paramArray.add(value);
                }
            }
            return paramArray.toArray();
        }

        /**Set of characters that qualify as parameter separators, indicating that a parameter name in a SQL String has ended. */
        private static final char[]   PARAMETER_SEPARATORS = new char[] { '"', '\'', ':', '&', ',', ';', '(', ')', '|', '=', '+', '-', '*', '%', '/', '\\', '<', '>', '^' };
        /** Set of characters that qualify as comment or quotes starting characters.*/
        private static final String[] START_SKIP           = new String[] { "'", "\"", "--", "/*" };
        /**Set of characters that at are the corresponding comment or quotes ending characters. */
        private static final String[] STOP_SKIP            = new String[] { "'", "\"", "\n", "*/" };

        //-------------------------------------------------------------------------
        // Core methods used by NamedParameterJdbcTemplate and SqlQuery/SqlUpdate
        //-------------------------------------------------------------------------
        public static ParsedSql getParsedSql(final String originalSql) {
            ParsedSql parSQL = new ParsedSql(originalSql);
            //
            //1.关键参数定义
            List<String> parameterNames = new ArrayList<>();
            List<int[]> parameterIndexes = new ArrayList<>();
            int namedParameterCount = 0;//带有名字参数的总数
            int unnamedParameterCount = 0;//无名字参数总数
            int totalParameterCount = 0;//参数总数
            //
            //2.分析SQL，提取出SQL中参数信息
            Objects.requireNonNull(originalSql, "SQL must not be null");
            Set<String> namedParameters = new HashSet<>();
            char[] statement = originalSql.toCharArray();
            int i = 0;
            while (i < statement.length) {
                int skipToPosition = skipCommentsAndQuotes(statement, i);//从当前为止掠过的长度
                if (i != skipToPosition) {
                    if (skipToPosition >= statement.length) {
                        break;
                    }
                    i = skipToPosition;
                }
                char c = statement[i];
                if (c == ':' || c == '&') {
                    int j = i + 1;
                    if (j < statement.length && statement[j] == ':' && c == ':') {
                        i = i + 2;// Postgres-style "::" casting operator - to be skipped.
                        continue;
                    }
                    while (j < statement.length && !isParameterSeparator(statement[j])) {
                        j++;
                    }
                    if (j - i > 1) {
                        String parameter = originalSql.substring(i + 1, j);
                        if (!namedParameters.contains(parameter)) {
                            namedParameters.add(parameter);
                            namedParameterCount++;
                        }
                        parameterNames.add(parameter);
                        parameterIndexes.add(new int[] { i, j });//startIndex, endIndex
                        totalParameterCount++;
                    }
                    i = j - 1;
                } else if (c == '?') {
                    unnamedParameterCount++;
                    totalParameterCount++;
                }
                i++;
            }
            parSQL.namedParameterCount = namedParameterCount;/*带有名字参数的总数*/
            parSQL.unnamedParameterCount = unnamedParameterCount;/*匿名参数的总数*/
            parSQL.totalParameterCount = totalParameterCount;/*总共参数个数*/
            parSQL.parameterIndexes = parameterIndexes;
            parSQL.parameterNames = parameterNames;
            return parSQL;
        }

        /** Skip over comments and quoted names present in an SQL statement */
        private static int skipCommentsAndQuotes(final char[] statement, final int position) {
            for (int i = 0; i < START_SKIP.length; i++) {
                if (statement[position] == START_SKIP[i].charAt(0)) {
                    boolean match = true;
                    for (int j = 1; j < START_SKIP[i].length(); j++) {
                        if (!(statement[position + j] == START_SKIP[i].charAt(j))) {
                            match = false;
                            break;
                        }
                    }
                    if (match) {
                        int offset = START_SKIP[i].length();
                        for (int m = position + offset; m < statement.length; m++) {
                            if (statement[m] == STOP_SKIP[i].charAt(0)) {
                                boolean endMatch = true;
                                int endPos = m;
                                for (int n = 1; n < STOP_SKIP[i].length(); n++) {
                                    if (m + n >= statement.length) {
                                        return statement.length;// last comment not closed properly
                                    }
                                    if (!(statement[m + n] == STOP_SKIP[i].charAt(n))) {
                                        endMatch = false;
                                        break;
                                    }
                                    endPos = m + n;
                                }
                                if (endMatch) {
                                    return endPos + 1;// found character sequence ending comment or quote
                                }
                            }
                        }
                        // character sequence ending comment or quote not found
                        return statement.length;
                    }
                }
            }
            return position;
        }

        /** Determine whether a parameter name ends at the current position, that is, whether the given character qualifies as a separator. */
        private static boolean isParameterSeparator(final char c) {
            if (Character.isWhitespace(c)) {
                return true;
            }
            for (char separator : PARAMETER_SEPARATORS) {
                if (c == separator) {
                    return true;
                }
            }
            return false;
        }
    }
}
